/***************************************************************************
 *   Copyright (C) 2011 by TinySofts                                       *
 *   tinysofts@gmail.com                                                   *
 *   All Rights Reserved.                                                  *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   This file is partly based on   GoldDict/bgl_babylon.cpp               *
 ***************************************************************************/

/*
    Babylon Dict Lib
    Status:O;Open;ReleaseReady;M
*/


#include <algorithm>
#include<string.h>
#include<stdio.h>

#include "babylondict.h"

#include <iostream>
using namespace std;

#ifdef _WIN32
#include <io.h>
#define DUP _dup
#define _CRT_SECURE_NO_WARNINGS
#else
#define DUP dup
#endif

using std::string;

BabylonDict::BabylonDict()
{
    file = NULL;
}

BabylonDict::BabylonDict( std::string filename )
{
    m_filename = filename;
    file = NULL;
}

BabylonDict::~BabylonDict()
{
    close();
}

int BabylonDict::open()
{
    FILE *f;
    unsigned char buf[6];
    int i;
    f = fopen(m_filename.c_str(), "rb");
    if(f == NULL)
        return -1; /*Open File Error*/

    i = fread(buf, 1, 6, f);
    /* First four bytes: BGL signature 0x12340001 or 0x12340002 (big-endian) */
    if( i < 6 || memcmp(buf, "\x12\x34\x00", 3) || buf[3] == 0 || buf[3] > 2 )
    {
        fclose(f);
        return -2; /*Dict File Signature Error*/
    }

    /* Calculate position of gz header */
    i = buf[4] << 8 | buf[5];
    if( i < 6 )
    {
        fclose(f);
        return -3; /*Get gzHeader Error*/
    }
    if(fseek(f, i, SEEK_SET)) /* can't seek - emulate */
        for(int j=0;j < i - 6;j++)
            fgetc(f);

    if(ferror(f) || feof(f))
    {
        fclose(f);
        return -4; /*File Size Error*/
    }

    /*we need to flush the file because otherwise some nfs mounts don't seem
      to properly update the file position for the following reopen */
    fflush(f);

    file = gzdopen(DUP(fileno(f)), "r");
    fclose(f);
    if(file == NULL)
        return -5; /*Format Error*/

    return 0;
}


void BabylonDict::close()
{
    if (file)
    {
        gzclose( file );
        file = 0;
    }
}

bool BabylonDict::readBlock( bgl_block &block )
{
    if(gzeof(file) || file == NULL)
        return false;

    block.length = bgl_readnum( 1 );
    block.type = block.length & 0xf;
    if( block.type == 4 )
        return false; // end of file marker
    block.length >>= 4;
    block.length = block.length < 4 ? bgl_readnum(block.length+1)
                                    : block.length-4 ;
    if( block.length )
    {
        block.data = (char *)malloc( block.length );
        gzread( file, block.data, block.length );
    }
    return true;
}

unsigned int BabylonDict::bgl_readnum( int bytes )
{
    unsigned char buf[4];
    unsigned val = 0;
    if (bytes < 1 || bytes > 4)
        return (0);

    gzread( file, buf, bytes );
    for(int i=0;i<bytes;i++)
        val= (val << 8) | buf[i];
    return val;
}

bool BabylonDict::reset()
{
    gzseek(file, 0, SEEK_SET);
    return true;
}

bool BabylonDict::read()
{
    if(file == NULL)
        return false;

    bgl_block block;
    unsigned int pos;
    unsigned int type;
    std::string headword;
    std::string definition;

    bool isUtf8File = false;
    m_numEntries = 0;

    while(readBlock(block))
    {
        headword.clear();
        definition.clear();
        switch( block.type )
        {
        case 0:
            switch( block.data[0] )
            {
            case 8:
                type = (unsigned int)block.data[2];
                if(type > 64)
                    type -= 65;
                if ( type >= 14 )
                    type = 0;
                m_defaultCharset = bgl_charset[type];
                break;
            default:
                //fprintf(debug,"%s\n",block.data);
                break;
            }
            break;
        case 1:
        case 10:
            //Only count entries
            m_numEntries++;
        case 3:
            pos = 2;
            switch( block.data[1] )
            {
            case 1:
                headword.reserve( block.length - 2 );
                for(unsigned int a=0;a<block.length-2;a++)
                    headword += block.data[pos++];
                m_title = headword;
                break;
            case 2:
                headword.reserve( block.length - 2 );
                for(unsigned int a=0;a<block.length-2;a++)
                    headword += block.data[pos++];
                m_author = headword;
                break;
            case 3:
                headword.reserve( block.length - 2 );
                for(unsigned int a=0;a<block.length-2;a++)
                    headword += block.data[pos++];
                m_email = headword;
                break;
            case 4:
                headword.reserve( block.length - 2 );
                for(unsigned int a=0;a<block.length-2;a++)
                    headword += block.data[pos++];
                m_copyright = headword;
                break;
            case 7:
                headword = bgl_language[(unsigned char)(block.data[5])];
                m_sourceLang = headword;
                break;
            case 8:
                headword = bgl_language[(unsigned char)(block.data[5])];
                m_targetLang = headword;
                break;
            case 9:
                headword.reserve( block.length - 2 );
                for(unsigned int a=0;a<block.length-2;a++) {
                    if (block.data[pos] == '\r') {
                    }
                    else if (block.data[pos] == '\n') {
                        headword += "<br>";
                    }
                    else {
                        headword += block.data[pos];
                    }
                    pos++;
                }
                m_description = headword;
                break;
            case 11:
                icon.resize( block.length - 2 );
                memcpy( &icon.front(), &(block.data[ 2 ]), icon.size() );
                break;
            case 17:
                if ( block.length >= 5 && (unsigned char) block.data[ 4 ] == 0x80 )
                    isUtf8File = true;
                break;
            case 26:
                type = (unsigned int)block.data[2];
                if(type > 64 )
                    type -= 65;
                if ( type >= 14 )
                    type = 0;
                if (m_sourceCharset.empty())
                    m_sourceCharset = bgl_charset[type];
                break;
            case 27:
                type = (unsigned int)block.data[2];
                if(type > 64)
                    type -= 65;
                if (type >= 14)
                    type = 0;
                if (m_targetCharset.empty())
                    m_targetCharset = bgl_charset[type];
                break;
            default:
                break;
            }
            break;
        default:
            ;
        }
        if(block.length)
            free(block.data);
    }

    gzseek(file, 0, SEEK_SET);
    if(isUtf8File)
    {
        m_defaultCharset = "UTF-8";
        m_sourceCharset = "UTF-8";
        m_targetCharset = "UTF-8";
    }

    convertToUtf8(m_title, TARGET_CHARSET);
    convertToUtf8(m_author, TARGET_CHARSET);
    convertToUtf8(m_email, TARGET_CHARSET);
    convertToUtf8(m_copyright, TARGET_CHARSET);
    convertToUtf8(m_description, TARGET_CHARSET);
    /*
    printf("Default charset: %s\nSource Charset: %s\nTargetCharset: %s\n",
           m_defaultCharset.c_str(), m_sourceCharset.c_str(), m_targetCharset.c_str());

    cout<<"m_title:"<<m_title<<endl;
    cout<<"m_author:"<<m_author<<endl;
    cout<<"m_email:"<<m_email<<endl;
    cout<<"m_copyright:"<<m_copyright<<endl;
    cout<<"m_description:"<<m_description<<endl;
    cout<<"m_numEntries:"<<m_numEntries<<endl;
    //*/
    return true;
}

bgl_entry BabylonDict::readEntryE( ResourceHandler * resourceHandler )
{
    bgl_entry entry;
    if( file == NULL )
    {
        entry.headword = "";
        return entry;
    }

    bgl_block block;
    unsigned int len, pos;
    std::string headword, displayedHeadword;
    std::string definition;
    std::string temp;
    std::vector<std::string> alternates;
    std::string alternate;

    std::string root;
    bool defBodyEnded = false;

    while(readBlock(block))
    {
        switch( block.type )
        {
        case 2:
            {
                pos = 0;
                len = (unsigned char)block.data[pos++];
                std::string filename( block.data+pos, len );
                pos += len;
                if ( resourceHandler )
                    resourceHandler->handleBabylonResource(filename,
                                                  block.data + pos,
                                                  block.length - pos);
                break;
            }
        case 1:
        case 10:
            alternate.clear();
            headword.clear();
            displayedHeadword.clear();
            root.clear();
            definition.clear();
            temp.clear();
            pos = 0;

            //Headword
            len=0;
            len = (unsigned char)block.data[pos++];
            headword.reserve(len);
            for(unsigned int a=0;a<len;a++)
                headword += block.data[pos++];
            convertToUtf8(headword, SOURCE_CHARSET);

            //Definition
            len = 0;
            len = (unsigned char)block.data[pos++] << 8;
            len |= (unsigned char)block.data[pos++];
            definition.reserve(len);
            for(unsigned int a=0;a<len;a++)
            {
                if((unsigned char)block.data[pos] == 0x0a)
                {
                    definition += "<br>";
                    pos++;
                }
                else if ((unsigned char)block.data[pos] == 6)
                {
                    //Something
                    pos += 2;
                    ++a;
                    definition += " ";
                }
                else if ((unsigned char)block.data[pos]>=0x40 &&
                         len-a>=2 &&
                         (unsigned char)block.data[pos+1] == 0x18)
                {
                    //Hidden displayed headword (a displayed headword which
                    //contains some garbage and shouldn't probably be visible).
                    unsigned length = (unsigned char)block.data[pos]-0x3F;
                    if (length > len-a-2)
                    {
                        fprintf( stderr,
                                 "Hidden displayed headword is too large %s\n",
                                 headword.c_str());
                        pos += len-a;
                        break;
                    }
                    pos += length+2;
                    a += length+1;
                }
                else if ((unsigned char)block.data[pos] == 0x18)
                {
                    // Displayed headword
                    unsigned length = (unsigned char)block.data[pos+1];
                    if (length > len-a-2)
                    {
                        fprintf( stderr,
                                 "Displayed headword's length is too large for headword %s\n",
                                 headword.c_str());
                        pos += len - a;
                        break;
                    }
                    displayedHeadword = std::string(block.data+pos+2, length);
                    pos += length+2;
                    a += length+1;
                }
                else if ( block.data[ pos ] == 0x28 && defBodyEnded &&
                         len-a >= 3 )
                {
                    //2-byte sized displayed headword
                    unsigned length = (unsigned char)block.data[ pos + 1 ];
                    length <<= 8;
                    length += (unsigned char)block.data[ pos + 2 ];
                    if ( length > len - a - 3 )
                    {
                        fprintf(stderr,
                                "2-byte sized displayed headword for %s is too large\n",
                                headword.c_str());
                        pos += len - a;
                        break;
                    }

                    displayedHeadword = std::string(block.data+pos+3, length);
                    pos += length + 3;
                    a += length + 2;
                }
                else if((unsigned char)block.data[pos]==0x50 && len-a-1>=2 &&
                    (unsigned char)block.data[pos+1]==0x1B)
                {
                    // 1-byte-sized transcription
                    unsigned length = (unsigned char)block.data[pos + 2 ];
                    if (length > len-a-3)
                    {
                        fprintf(stderr,
                                 "1-byte-sized transcription's length is too large for headword %s\n",
                                 headword.c_str());
                        pos += len-a;
                        break;
                    }
                    std::string transcription(block.data+pos+3, length);

                    definition = transcription + definition;
                    pos += length + 3;
                    a += length + 2;
                }
                else if ((unsigned char)block.data[pos] == 0x60 && len-a-1 >= 3 &&
                    (unsigned char)block.data[pos + 1 ] == 0x1B)
                {
                    //2-byte-sized transcription
                    unsigned length = (unsigned char)block.data[pos + 2 ];
                    length <<= 8;
                    length += (unsigned char)block.data[pos+3];
                    if (length > len - a - 4)
                    {
                        fprintf(stderr,
                                "2-byte-sized transcription's length is too large for headword %s\n",
                                headword.c_str());
                        pos += len - a;
                        break;
                    }
                    std::string transcription(block.data+pos+4, length);
                    definition = transcription + definition;

                    pos += length + 4;
                    a += length + 3;
                }
                else if ( (unsigned char)block.data[pos] >= 0x40 &&
                          len - a >= 2 &&
                          (unsigned char)block.data[pos + 1 ] == 0x1B)
                {
                    // Hidden transcription (a transcription which is usually the same
                    // as the headword and shouldn't probably be visible).
                    unsigned length = (unsigned char)block.data[ pos ] - 0x3F;
                    if (length > len-a-2)
                    {
                        fprintf(stderr,
                                "Hidden transcription is too large %s\n",
                                headword.c_str() );
                        pos += len - a;
                        break;
                    }
                    pos += length + 2;
                    a += length + 1;
                }
                else if ((unsigned char)block.data[pos] == 0x1E)
                {
                    // Resource reference begin marker
                    definition += m_resourcePrefix;
                    ++pos;
                }
                else if ((unsigned char)block.data[pos] == 0x1F)
                {
                    // Resource reference end marker
                    ++pos;
                }
                else if((unsigned char)block.data[pos] < 0x20)
                {
                    if(a <= len - 3 && block.data[pos] == 0x14 &&
                       block.data[pos+1] == 0x02)
                    {
                        int index = (unsigned char)block.data[pos+2]-0x30;
                        if (index >= 0 && index <= 10)
                        {
                            definition = partOfSpeech[index]+definition;
                        }
                        pos += 3;
                        a += 2;
                        //pos += len - a;
                        //break;
                    }
                    else if(block.data[pos] == 0x14)
                    {
                        defBodyEnded = true; // Presumably
                        pos++;
                    }
                    else if ((unsigned char)block.data[pos] == 0x1A)
                    {
                        unsigned length = (unsigned char)block.data[ pos + 1 ];
                        if (length <= 10)
                        {//0x1A identifies two different data types.
                         //data about the Hebrew root should be shorter then
                         //10 bytes, and in the other data type the byte
                         //after 0x1A is > 10 (at least it is in Bybylon's
                         //Hebrew dictionaries).
                            root = std::string( block.data + pos + 2, length );
                            std::reverse(root.begin(),root.end());
                            definition += " (" + root + ")";
                            pos += length + 2;
                            a += length + 1;
                        }
                        else
                            pos++;
                    }
                    else
                    {
                        definition += block.data[pos++];
                    }
                }
                else
                    definition += block.data[pos++];
            }
            //definitionUtf8(definition);
            if (displayedHeadword.size())
                convertToUtf8( displayedHeadword, TARGET_CHARSET );


            //Alternate forms
            while( pos != block.length )
            {
                len = (unsigned char)block.data[pos++];
                alternate.reserve( len );
                for(unsigned int a=0;a<len;a++) alternate += block.data[pos++];
                convertToUtf8( alternate, SOURCE_CHARSET );
                alternates.push_back( alternate );
                alternate.clear();
            }

            // Try adding displayed headword to the list of alts
            if ( headword != displayedHeadword )
            {
                // Only add displayed headword if the normal one has two or more digits.
                // This would indicate some irregularity in it (like e.g. if it serves
                // as some kind of an identifier instead of being an actual headword)
                int totalDigits = 0;
                for( char const * p = headword.c_str(); *p; ++p )
                {
                    if ( *p >= '0' && *p <= '9' )
                    {
                        if ( ++totalDigits > 1 )
                            break;
                    }
                }

                if ( totalDigits > 1 )
                {
                    alternates.push_back(displayedHeadword);
                }
            }

            entry.headword = headword;
            entry.displayedHeadword = displayedHeadword;
            entry.definition = definition;
            entry.alternates = alternates;

            if(block.length)
                free(block.data);

            //Some dictionaries can in fact have an empty headword, so we
            //make it non-empty here to differentiate between the end of entries.
            if ( entry.headword.empty() )
                entry.headword += ' ';
            return entry;
            break;
        default:
            ;
        }
        if(block.length)
            free(block.data);
    }
    entry.headword = "";
    return entry;
}

bgl_entry BabylonDict::readEntry( ResourceHandler *)
{
    bgl_entry entry;
    if(file == NULL)
    {
        entry.headword = "";
        return entry;
    }
    bgl_block block;
    uint len, pos;
    std::string headword;
    std::string definition;
    std::string temp;
    std::vector<std::string> alternates;
    std::string alternate;

    while(readBlock(block))
    {
        switch(block.type)
        {
        case 1:
        case 10:
            alternate.clear();
            headword.clear();
            definition.clear();
            temp.clear();
            pos = 0;
            //Headword
            len = 0;
            len = (unsigned char)block.data[pos++];
            headword.reserve(len);
            for(uint a=0;a<len;a++)
                headword += block.data[pos++];
            convertToUtf8(headword, SOURCE_CHARSET );

            //Definition
            len=0;
            len=(unsigned char)block.data[pos++] << 8;
            len |= (unsigned char)block.data[pos++];
            definition.reserve(len);

            for(uint a=0;a<len;a++)
            {
                if((unsigned char)block.data[pos]<0x20)
                {
                    if(a<len-3 && block.data[pos]==0x14 &&
                       block.data[pos+1]==0x02)
                    {
                        definition=partOfSpeech[(unsigned char)block.data[pos+2]
                                                -0x30]
                                   +" "+definition;
                    }
                    pos += len - a;
                    break;
                }
                else
                {
                    definition += block.data[pos++];
                }
            }
            //definitionUtf8(definition);

            //Alternate forms
            while(pos!=block.length)
            {
                len = (unsigned char)block.data[pos++];

                alternate.reserve( len );
                for(uint a=0;a<len;a++)
                    alternate += block.data[pos++];
                convertToUtf8( alternate, SOURCE_CHARSET );
                alternates.push_back( alternate );
                alternate.clear();
            }

            entry.headword = headword;
            entry.definition = definition;
            entry.alternates = alternates;
            return entry;
            break;
        default:
            ;
        }
        if(block.length)
            free(block.data);
    }
    entry.headword = "";
    return entry;
}

string BabylonDict::definitionUtf8(string & d)
{
    QByteArray t(d.c_str());
    int idx = t.indexOf("||");
    if(idx>=0)
    {
        string r(t.left(idx).data());
        convertToUtf8(r,SOURCE_CHARSET);
        string tmp(t.mid(idx).data());
        convertToUtf8(tmp,TARGET_CHARSET);
        d=string(r.c_str());
        d+=tmp;
    }
    else
    {
        convertToUtf8(d,TARGET_CHARSET);
    }
    return d;
}

bool BabylonDict::convertToUtf8( std::string &s, unsigned int type )
{
    std::string charset;
    switch(type)
    {
      case DEFAULT_CHARSET:
        if(!m_defaultCharset.empty())
            charset = m_defaultCharset;
        else
            charset = m_sourceCharset;
        break;
      case SOURCE_CHARSET:
        if(!m_sourceCharset.empty())
            charset = m_sourceCharset;
        else
            charset = m_defaultCharset;
        break;
      case TARGET_CHARSET:
        if(!m_targetCharset.empty())
            charset = m_targetCharset;
        else
            charset = m_defaultCharset;
        break;
      default:
        ;
    }
    QTextCodec *codec = QTextCodec::codecForName(charset.c_str());
    if(codec==NULL)
        return false;
    QByteArray t;
    t.append(codec->toUnicode(s.c_str()));
    s = std::string(t.data());
    return true;
}

QByteArray BabylonDict::lookUp(QString word)
{
    reset();
    QByteArray r;
    bgl_entry e;
    int cnt=0;
    while(true) {
        e = readEntryE();
        cnt++;
        e.headword=removePostfix(e.headword);
        if(e.headword.empty())
            break;
        QString w(e.headword.c_str());
        if(w < word)
            continue;
        else {
            definitionUtf8(e.definition);
            transcriptUtf8(e.definition);
            r.append(w);
            r.append("\n");
            r.append(e.definition.c_str());
            break;
        }
    }
    return r;
}


QByteArray BabylonDict::lookUpDict(const QString & word)
{
    return lookUp(word);
}

QByteArray BabylonDict::lookUpDict2(const QString & word)
{
   return lookUp(word);
}

bool BabylonDict::init()
{
    if(open()==0)
    {
       return read();
    }
    else
        return false;
}

bool BabylonDict::setPath(QString path, QString file)
{
    QByteArray f;
    f.append(file);
    m_filename=string(f.data());
    return true;
}

// Removes the $1$-like postfix
string removePostfix(string const & in)
{
    if(in.size() && in[in.size()-1] == '$')
    {
        //Find the end of it and cut it, barring any unexpectedness
        for(long x = in.size()-2; x >= 0; x--)
        {
            if(in[x] == '$')
                return in.substr(0, x);
            else
                if(!isdigit(in[ x ]))
                    break;
        }
    }
    return in;
}

string transcriptUtf8(string & ts)
{
    QByteArray t(ts.c_str());
    int idx=t.indexOf("<charset c=T>");
    while(idx>=0)
    {
        QByteArray uni=t.mid(idx+strlen("<charset c=T>"),4);
        bool r;
        uint value=uni.toInt(&r,16);
        QChar ch(value);
        QString ustr(ch);
        t=t.replace(idx,strlen("<charset c=T>02D0;</charset>"),
                    ustr.toUtf8());
        idx=t.indexOf("<charset c=T>");
    }
    idx=t.indexOf("&#");
    while(idx>=0)
    {
        QByteArray uni=t.mid(idx+2,4);
        uint value=uni.toInt();
        QChar ch(value);
        QString ustr(ch);
        t=t.replace(idx,strlen("&#0130;"),ustr.toUtf8());
        idx=t.indexOf("&#");
    }
    ts=string(t.data());
    return ts;
}
